mod clipboard_data_ref;
mod clipboard_impl;
mod cliprdr_backend;
mod os_clipboard;
mod remote_format_registry;
mod utils;

use std::sync::mpsc as mpsc_sync;

use ironrdp_cliprdr::backend::{ClipboardMessageProxy, CliprdrBackend, CliprdrBackendFactory};
use ironrdp_cliprdr::pdu::{
    ClipboardFormat, ClipboardFormatId, ClipboardGeneralCapabilityFlags, FormatDataRequest, FormatDataResponse,
};
use tracing::error;
use windows::core::{s, Error};
pub use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::{E_ACCESSDENIED, FALSE, LPARAM, LRESULT, WPARAM};
use windows::Win32::System::DataExchange::{AddClipboardFormatListener, RemoveClipboardFormatListener};
use windows::Win32::System::LibraryLoader::GetModuleHandleA;
use windows::Win32::UI::Shell::{RemoveWindowSubclass, SetWindowSubclass};
use windows::Win32::UI::WindowsAndMessaging::{
    CreateWindowExA, DefWindowProcA, RegisterClassA, CW_USEDEFAULT, WINDOW_EX_STYLE, WM_USER, WNDCLASSA, WS_POPUP,
};

use self::clipboard_impl::{clipboard_subproc, WinClipboardImpl};
use self::cliprdr_backend::WinCliprdrBackend;

const BACKEND_CHANNEL_SIZE: usize = 8;
const WM_CLIPRDR_BACKEND_EVENT: u32 = WM_USER;

pub type WinCliprdrResult<T> = Result<T, WinCliprdrError>;

#[derive(Debug)]
pub enum WinCliprdrError {
    AddClipboardFormatListener,
    FormatsEnumeration,
    ClipboardAccessDenied,
    ClipboardOpen,
    ClipboardEmpty,
    Utf16Conversion,
    ClipboardData,
    SetClipboardData,
    WindowSubclass,
    Alloc,
    DataReceiveTimeout,
    RenderFormat,
    WinAPI(Error),
}

impl core::fmt::Display for WinCliprdrError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            WinCliprdrError::AddClipboardFormatListener => write!(f, "failed to register clipboard format listener"),
            WinCliprdrError::FormatsEnumeration => {
                write!(f, "failed to enumerate formats available in the current clipboard")
            }
            WinCliprdrError::ClipboardAccessDenied => write!(f, "clipboard is busy or denied"),
            WinCliprdrError::ClipboardOpen => write!(f, "failed to open the clipboard"),
            WinCliprdrError::ClipboardEmpty => write!(f, "failed to empty the clipboard"),
            WinCliprdrError::Utf16Conversion => write!(f, "failed to convert UTF-16 string to UTF-8"),
            WinCliprdrError::ClipboardData => write!(f, "failed to get current clipboard data"),
            WinCliprdrError::SetClipboardData => write!(f, "failed to set clipboard data"),
            WinCliprdrError::WindowSubclass => write!(f, "failed to subclass window"),
            WinCliprdrError::Alloc => write!(f, "failed to allocate global memory"),
            WinCliprdrError::DataReceiveTimeout => write!(f, "failed to receive data from remote clipboard"),
            WinCliprdrError::RenderFormat => write!(f, "failed to render clipboard format"),
            WinCliprdrError::WinAPI(_error) => write!(f, "WinAPI error"),
        }
    }
}

impl core::error::Error for WinCliprdrError {
    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
        match self {
            WinCliprdrError::AddClipboardFormatListener => None,
            WinCliprdrError::FormatsEnumeration => None,
            WinCliprdrError::ClipboardAccessDenied => None,
            WinCliprdrError::ClipboardOpen => None,
            WinCliprdrError::ClipboardEmpty => None,
            WinCliprdrError::Utf16Conversion => None,
            WinCliprdrError::ClipboardData => None,
            WinCliprdrError::SetClipboardData => None,
            WinCliprdrError::WindowSubclass => None,
            WinCliprdrError::Alloc => None,
            WinCliprdrError::DataReceiveTimeout => None,
            WinCliprdrError::RenderFormat => None,
            WinCliprdrError::WinAPI(error) => Some(error),
        }
    }
}

impl From<Error> for WinCliprdrError {
    fn from(err: Error) -> Self {
        if err.code() == E_ACCESSDENIED {
            WinCliprdrError::ClipboardAccessDenied
        } else {
            WinCliprdrError::WinAPI(err)
        }
    }
}

/// Sent from the clipboard backend shim to the actual WinAPI subproc event loop
#[derive(Debug)]
pub(crate) enum BackendEvent {
    // Events generated by OS event loop
    ClipboardUpdated,
    RenderFormat(ClipboardFormatId),
    RenderAllFormats,

    // PDU processing events
    DowngradedCapabilities(ClipboardGeneralCapabilityFlags),
    RemoteFormatList(Vec<ClipboardFormat>),
    FormatDataRequest(FormatDataRequest),
    FormatDataResponse(FormatDataResponse<'static>),
    RemoteRequestsFormatList,
}

/// Windows RDP client clipboard implementation.
///
/// IronRDP client implementation should provide raw window handle and message proxy to send
/// messages from the backend to `CLIPRDR` SVC.
///
/// [`WinClipboard`] instance holds ownership of the actual clipboard backend processing logic
/// and should be kept alive during the whole lifetime of the application.
///
/// This type is not thread safe, it should be used only in the main thread with a windows event
/// loop. (GUI thread). However, backend factory returned by [`WinClipboard::backend_factory`]
/// can be safely used in other threads.
pub struct WinClipboard {
    window: HWND,
    backend_tx: mpsc_sync::SyncSender<BackendEvent>,

    /// From MS docs:
    /// ```text
    /// You cannot use the subclassing helper functions to subclass a window across threads
    /// ```
    ///
    /// Therefore this type should be non-Send and non-Sync to prevent incorrect use.
    _thread_marker: core::marker::PhantomData<*const ()>,
}

impl WinClipboard {
    /// Creates new clipboard instance.
    ///
    /// Under the hood, a hidden window is created for capturing the clipboard events.
    pub fn new(message_proxy: impl ClipboardMessageProxy + 'static) -> WinCliprdrResult<Self> {
        extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
            // SAFETY: default handler
            unsafe { DefWindowProcA(window, message, wparam, lparam) }
        }

        // SAFETY: low-level WinAPI call
        let instance = unsafe { GetModuleHandleA(None)? };
        let window_class = s!("IronRDPClipboardMonitor");
        let wc = WNDCLASSA {
            hInstance: instance.into(),
            lpszClassName: window_class,
            lpfnWndProc: Some(wndproc),
            ..Default::default()
        };

        // SAFETY: low-level WinAPI call
        let atom = unsafe { RegisterClassA(&wc) };
        if atom == 0 {
            return Err(WinCliprdrError::from(Error::from_thread()));
        }

        // SAFETY: low-level WinAPI call
        let window = unsafe {
            CreateWindowExA(
                WINDOW_EX_STYLE::default(),
                window_class,
                None,
                WS_POPUP,
                CW_USEDEFAULT,
                CW_USEDEFAULT,
                CW_USEDEFAULT,
                CW_USEDEFAULT,
                None,
                None,
                Some(instance.into()),
                None,
            )?
        };

        if window.is_invalid() {
            return Err(WinCliprdrError::from(Error::from_thread()));
        }
        // Init clipboard processing for WinAPI event loop
        //
        // SAFETY: `window` is a valid window handle
        unsafe { AddClipboardFormatListener(window)? };

        let (backend_tx, backend_rx) = mpsc_sync::sync_channel(BACKEND_CHANNEL_SIZE);

        let ctx = Box::new(WinClipboardImpl::new(window, message_proxy, backend_rx));

        // We need to receive winapi messages in the main thread, so we need to add a subclass to
        // the window.
        //
        // SAFETY: `window` is a valid window handle, `clipboard_subproc` is in the static memory,
        // `ctx` is valid and its ownership is transferred to the subclass via `into_raw`.
        let winapi_result = unsafe {
            SetWindowSubclass(
                window,
                Some(clipboard_subproc),
                0,
                Box::into_raw(ctx).expose_provenance(),
            )
        };

        if winapi_result == FALSE {
            return Err(WinCliprdrError::WindowSubclass);
        }

        Ok(Self {
            window,
            backend_tx,
            _thread_marker: Default::default(),
        })
    }

    /// Returns clipboard backend factory suitable for making backend instances for `CLIPRDR` SVC.
    pub fn backend_factory(&self) -> Box<dyn CliprdrBackendFactory + Send> {
        Box::new(WinCliprdrBackendFactory {
            tx: self.backend_tx.clone(),
            window: self.window,
        })
    }
}

impl Drop for WinClipboard {
    fn drop(&mut self) {
        // Remove clipboard processing from WinAPI event loop

        // SAFETY: Format listener was registered in the `new` method previously.
        if let Err(err) = unsafe { RemoveClipboardFormatListener(self.window) } {
            error!("Failed to remove clipboard listener: {}", err)
        }

        // SAFETY: Subclass was registered in the `new` method previously.
        if !unsafe { RemoveWindowSubclass(self.window, Some(clipboard_subproc), 0) }.as_bool() {
            error!("Failed to remove window subclass")
        }
    }
}

/// Windows-specific clipboard backend factory
struct WinCliprdrBackendFactory {
    tx: mpsc_sync::SyncSender<BackendEvent>,
    window: HWND,
}

// SAFETY: window handle is thread safe for PostMessageW usage
unsafe impl Send for WinCliprdrBackendFactory {}

impl CliprdrBackendFactory for WinCliprdrBackendFactory {
    fn build_cliprdr_backend(&self) -> Box<dyn CliprdrBackend> {
        Box::new(WinCliprdrBackend::new(self.window, self.tx.clone()))
    }
}
